Powered by Prof. Carello

📝 Le Stringhe in C

Una guida completa alla gestione delle sequenze di caratteri nel linguaggio C

Tempo di lettura: ~35 minuti

🎯 Introduzione

Le stringhe rappresentano uno degli elementi più utilizzati nella programmazione, essendo fondamentali per la gestione di testi, nomi, messaggi e qualsiasi tipo di informazione testuale. A differenza di molti linguaggi di programmazione moderni che forniscono un tipo di dato dedicato per le stringhe, il linguaggio C adotta un approccio più "primitivo" e a basso livello: le stringhe sono array di caratteri terminati da un carattere speciale chiamato carattere null ('\0').

Questa scelta progettuale, sebbene possa sembrare limitante rispetto ad approcci più moderni, riflette la filosofia del C di fornire accesso diretto e controllo completo sulla memoria. Comprendere come funzionano le stringhe in C è essenziale non solo per utilizzare questo linguaggio, ma anche per capire come molti altri linguaggi gestiscono internamente le stringhe e per sviluppare una comprensione profonda della gestione della memoria.

💡 Obiettivi della Lezione

In questa lezione approfondiremo i seguenti argomenti:

  • Comprendere cosa sono le stringhe in C e come sono rappresentate in memoria
  • Imparare a dichiarare, inizializzare e gestire stringhe in C
  • Utilizzare le funzioni della libreria standard string.h
  • Confrontare stringhe e manipolarle in modo sicuro
  • Evitare errori comuni e vulnerabilità di sicurezza
  • Gestire l'input/output di stringhe correttamente
  • Allocare dinamicamente stringhe quando necessario

📚 Concetti Fondamentali

1.1 Cosa Sono le Stringhe in C

In C, una stringa è definita come un array di caratteri terminato dal carattere null ('\0'). Il carattere null, con valore ASCII 0, è fondamentale perché indica la fine della stringa. Senza questo terminatore, le funzioni C non saprebbero dove la stringa termina, portando a comportamenti indefiniti.

Rappresentazione in Memoria di una Stringa

Stringa: "CIAO"

[0]
'C'
67
[1]
'I'
73
[2]
'A'
65
[3]
'O'
79
[4]
'\0'
0

Nota: La stringa "CIAO" occupa 5 byte in memoria: 4 caratteri più il terminatore null.
I numeri sotto ogni carattere rappresentano i valori ASCII corrispondenti.

ℹ️ Il Carattere Null

Il carattere null '\0' non è lo stesso dello spazio ' ' o del carattere zero '0':

  • '\0' ha valore ASCII 0 (terminatore di stringa)
  • ' ' ha valore ASCII 32 (carattere spazio visibile)
  • '0' ha valore ASCII 48 (carattere cifra zero)

1.2 Differenza tra Carattere e Stringa

È fondamentale comprendere la distinzione tra un singolo carattere e una stringa in C, poiché vengono gestiti in modi completamente diversi.

Carattere Singolo (char)

Un carattere singolo si definisce con apici singoli ' ' e occupa esattamente 1 byte in memoria. Rappresenta un singolo valore ASCII.

char lettera = 'A';    // Un byte
char cifra = '7';      // Un byte
char simbolo = '@';    // Un byte

Stringa (array di char)

Una stringa si definisce con apici doppi " " e occupa N+1 byte in memoria, dove N è il numero di caratteri visibili e +1 è per il terminatore null.

char parola[] = "A";       // 2 byte: 'A' + '\0'
char numero[] = "7";       // 2 byte: '7' + '\0'
char simboli[] = "@";      // 2 byte: '@' + '\0'
⚠️ Errore Comune

Non confondere la sintassi:

char x = 'A';      // CORRETTO: carattere singolo
char y = "A";      // ERRORE: stai assegnando un puntatore a char
char z[] = 'A';    // ERRORE: gli array si inizializzano con stringhe o liste
char w[] = "A";    // CORRETTO: stringa di un carattere + '\0'

📋 Dichiarazione e Inizializzazione

2.1 Metodi di Dichiarazione

Esistono diversi modi per dichiarare e inizializzare stringhe in C. Ogni metodo ha le sue caratteristiche e casi d'uso specifici. Vediamoli in dettaglio.

2.1.1 Inizializzazione con Stringa Letterale

Il metodo più comune e diretto è utilizzare una stringa letterale tra doppi apici. Il compilatore calcola automaticamente la dimensione necessaria e aggiunge il terminatore null.

// Dimensione automatica - il compilatore conta i caratteri e aggiunge '\0'
char nome[] = "Mario";
// Equivale a: char nome[6] = {'M', 'a', 'r', 'i', 'o', '\0'};
// Dimensione: 6 byte (5 caratteri + 1 terminatore)

char saluto[] = "Ciao mondo!";
// Dimensione: 12 byte (11 caratteri + 1 terminatore)

char vuota[] = "";
// Dimensione: 1 byte (solo il terminatore '\0')
✓ Best Practice

Lascia che il compilatore calcoli la dimensione dell'array quando possibile. È più sicuro e meno soggetto a errori.

2.1.2 Dichiarazione con Dimensione Esplicita

Puoi specificare esplicitamente la dimensione dell'array. Questo è utile quando vuoi riservare spazio per stringhe che potrebbero crescere o per buffer di input.

// Dimensione esplicita maggiore del necessario
char cognome[50] = "Rossi";
// cognome[0]='R', cognome[1]='o', cognome[2]='s', cognome[3]='s', 
// cognome[4]='i', cognome[5]='\0', cognome[6...49]=0

// Buffer per input utente
char input[256] = "";  // Inizializzato vuoto, pronto per ricevere input

// Array per costruire stringhe dinamicamente
char messaggio[100];   // Non inizializzato - PERICOLOSO!
char buffer[100] = {0}; // Inizializzato tutto a '\0' - SICURO!
⚠️ Attenzione alla Dimensione

Se la dimensione specificata è troppo piccola per contenere la stringa e il terminatore null, otterrai un errore di compilazione:

char errore[3] = "CIAO";  // ERRORE: servono 5 byte, ne hai 3!

2.1.3 Inizializzazione Carattere per Carattere

Mostra esempio di inizializzazione carattere per carattere
// Inizializzazione esplicita elemento per elemento
char parola[4] = {'C', 'I', 'A', '\0'};
// Equivale a: char parola[] = "CIA";

// ERRORE COMUNE: dimenticare il terminatore null
char sbagliato[3] = {'C', 'I', 'A'};  // Manca '\0' - NON è una stringa valida!

// Inizializzazione parziale - il resto viene riempito con '\0'
char parziale[10] = {'H', 'I'};
// parziale[0]='H', parziale[1]='I', parziale[2...9]='\0'

2.1.4 Puntatori a Stringhe Letterali

Mostra esempio con puntatori
// Puntatore a stringa letterale (in memoria di sola lettura)
char *messaggio = "Hello, World!";

// ATTENZIONE: Non puoi modificare il contenuto!
messaggio[0] = 'h';  // ERRORE a runtime: segmentation fault!

// Ma puoi far puntare il puntatore a un'altra stringa
messaggio = "Ciao!";  // OK: ora punta a un'altra stringa letterale

💡 Array vs Puntatore

Questa è una distinzione fondamentale:

char str1[] = "Hello";     // Array modificabile in memoria stack
char *str2 = "Hello";      // Puntatore a stringa in memoria read-only

str1[0] = 'h';  // OK: modifica la copia in memoria stack
str2[0] = 'h';  // ERRORE: tenta di modificare memoria read-only!

2.2 Stringhe e Array nella Lezione sugli Array

🔗 Approfondimento

Per una comprensione completa degli array in C, inclusa la rappresentazione in memoria, l'aritmetica dei puntatori e gli array multidimensionali, consulta la lezione dedicata agli Array in C. Le stringhe sono casi speciali di array di caratteri, quindi tutti i concetti sugli array si applicano anche alle stringhe.

🛠️ Funzioni della Libreria string.h

La libreria standard C fornisce numerose funzioni per manipolare le stringhe, tutte dichiarate nell'header <string.h>. Queste funzioni sono ottimizzate e affidabili, e dovrebbero essere preferite a implementazioni personalizzate nella maggior parte dei casi.

3.1 Lunghezza di una Stringa - strlen()

La funzione strlen() restituisce il numero di caratteri nella stringa, escludendo il terminatore null. Questa funzione scorre la stringa finché non trova '\0'.

Mostra esempio strlen()
#include <stdio.h>
#include <string.h>

int main() {
    char testo[] = "Programmazione";
    
    size_t lunghezza = strlen(testo);
    printf("La stringa '%s' ha %zu caratteri\n", testo, lunghezza);
    // Output: La stringa 'Programmazione' ha 14 caratteri
    
    // ATTENZIONE: strlen() NON conta il '\0'
    printf("Byte occupati in memoria: %zu\n", sizeof(testo));
    // Output: Byte occupati in memoria: 15 (14 caratteri + '\0')
    
    // Stringa vuota
    char vuota[] = "";
    printf("Lunghezza stringa vuota: %zu\n", strlen(vuota));
    // Output: Lunghezza stringa vuota: 0
    
    return 0;
}
ℹ️ Tipo size_t

strlen() restituisce un valore di tipo size_t, un tipo unsigned definito in <stddef.h>. Per stamparlo correttamente, usa il formato %zu con printf().

3.2 Copiare Stringhe - strcpy() e strncpy()

Per copiare una stringa in un'altra, C fornisce strcpy(). È importante che la destinazione abbia spazio sufficiente per contenere l'intera stringa sorgente più il terminatore null.

3.2.1 strcpy() - Copia Standard

Mostra esempio strcpy()
#include <stdio.h>
#include <string.h>

int main() {
    char sorgente[] = "Hello";
    char destinazione[20];  // Assicurati che sia abbastanza grande!
    
    strcpy(destinazione, sorgente);
    printf("Sorgente: %s\n", sorgente);        // Output: Hello
    printf("Destinazione: %s\n", destinazione); // Output: Hello
    
    // Le due stringhe sono indipendenti
    sorgente[0] = 'h';
    printf("Dopo modifica sorgente: %s\n", sorgente);        // Output: hello
    printf("Destinazione invariata: %s\n", destinazione);     // Output: Hello
    
    return 0;
}
❌ Pericolo: Buffer Overflow

strcpy() non controlla la dimensione del buffer di destinazione! Se la stringa sorgente è più lunga dello spazio disponibile, si verifica un buffer overflow, causando comportamenti indefiniti:

char piccolo[5];
strcpy(piccolo, "Questa stringa è troppo lunga");  
// DISASTRO: scrive oltre i limiti di piccolo!
// Possibili conseguenze: corruzione memoria, crash, vulnerabilità sicurezza

3.2.2 strncpy() - Copia Sicura con Limite

Mostra esempio strncpy()
#include <stdio.h>
#include <string.h>

int main() {
    char sorgente[] = "Programmazione in C";
    char destinazione[10];
    
    // Copia al massimo 9 caratteri (lasciando spazio per '\0')
    strncpy(destinazione, sorgente, 9);
    destinazione[9] = '\0';  // IMPORTANTE: aggiungi '\0' manualmente!
    
    printf("Destinazione: %s\n", destinazione);  // Output: Programma
    
    return 0;
}
⚠️ Attenzione con strncpy()

strncpy() ha un comportamento particolare:

  • Se la sorgente è più corta di n caratteri, riempie il resto con '\0'
  • Se la sorgente è più lunga di n caratteri, NON aggiunge automaticamente '\0' alla fine!

Devi sempre aggiungere manualmente il terminatore null per sicurezza.

3.3 Concatenare Stringhe - strcat() e strncat()

3.3.1 strcat() - Concatenazione Standard

Mostra esempio strcat()
#include <stdio.h>
#include <string.h>

int main() {
    char destinazione[50] = "Buon";  // Deve avere spazio per la stringa finale!
    char sorgente[] = "giorno";
    
    strcat(destinazione, sorgente);
    printf("Risultato: %s\n", destinazione);  // Output: Buongiorno
    
    // Concatenazione multipla
    strcat(destinazione, " a tutti!");
    printf("Risultato finale: %s\n", destinazione);  // Output: Buongiorno a tutti!
    
    return 0;
}

3.3.2 strncat() - Concatenazione Sicura

Mostra esempio strncat()
#include <stdio.h>
#include <string.h>

int main() {
    char buffer[20] = "Hello ";
    char aggiunta[] = "World, this is a very long string!";
    
    // Concatena al massimo 10 caratteri
    strncat(buffer, aggiunta, 10);
    // strncat() aggiunge automaticamente '\0'!
    
    printf("Risultato: %s\n", buffer);  // Output: Hello World, th
    
    return 0;
}
✓ Vantaggio di strncat()

A differenza di strncpy(), strncat() aggiunge sempre il terminatore null automaticamente!

3.4 Confrontare Stringhe - strcmp() e strncmp()

In C, non puoi confrontare stringhe con gli operatori ==, <, > ecc. Questi operatori confronterebbero gli indirizzi di memoria, non il contenuto. Devi usare strcmp().

3.4.1 strcmp() - Confronto Completo

strcmp() confronta due stringhe lessicograficamente (ordine alfabetico) e restituisce:

Mostra esempio strcmp()
#include <stdio.h>
#include <string.h>

int main() {
    char str1[] = "Apple";
    char str2[] = "Banana";
    char str3[] = "Apple";
    
    // Confronto stringhe
    if (strcmp(str1, str2) == 0) {
        printf("str1 e str2 sono uguali\n");
    } else if (strcmp(str1, str2) < 0) {
        printf("str1 precede str2\n");  // Questo verrà stampato
    } else {
        printf("str1 segue str2\n");
    }
    
    // Verifica uguaglianza
    if (strcmp(str1, str3) == 0) {
        printf("str1 e str3 sono uguali\n");  // Questo verrà stampato
    }
    
    // ERRORE COMUNE: non usare == per confrontare stringhe!
    if (str1 == str3) {  // Confronta gli indirizzi, non il contenuto!
        printf("Questo potrebbe non essere stampato!\n");
    }
    
    return 0;
}
⚠️ Case Sensitivity

strcmp() distingue tra maiuscole e minuscole:

strcmp("Hello", "hello")  // Restituisce un valore ≠ 0
strcmp("HELLO", "hello")  // Restituisce un valore negativo

3.5 Cercare in una Stringa - strchr(), strstr()

3.5.1 strchr() - Cerca un Carattere

Mostra esempio strchr()
#include <stdio.h>
#include <string.h>

int main() {
    char frase[] = "Imparare il linguaggio C";
    
    // Cerca il carattere 'l'
    char *posizione = strchr(frase, 'l');
    
    if (posizione != NULL) {
        printf("Trovato 'l' alla posizione: %ld\n", posizione - frase);
        // Output: Trovato 'l' alla posizione: 9
        printf("Sottostringa da 'l': %s\n", posizione);
        // Output: Sottostringa da 'l': linguaggio C
    } else {
        printf("Carattere non trovato\n");
    }
    
    return 0;
}

3.5.2 strstr() - Cerca una Sottostringa

Mostra esempio strstr()
#include <stdio.h>
#include <string.h>

int main() {
    char testo[] = "Il linguaggio C è potente e flessibile";
    char cerca[] = "linguaggio";
    
    char *risultato = strstr(testo, cerca);
    
    if (risultato != NULL) {
        printf("Trovato '%s' alla posizione: %ld\n", cerca, risultato - testo);
        printf("Resto del testo: %s\n", risultato);
    } else {
        printf("Sottostringa non trovata\n");
    }
    
    return 0;
}

3.6 Tokenizzare una Stringa - strtok()

Mostra esempio strtok()
#include <stdio.h>
#include <string.h>

int main() {
    char frase[] = "Mario,Rossi,25,Informatica";
    char delimitatori[] = ",";
    
    // Prima chiamata: passa la stringa
    char *token = strtok(frase, delimitatori);
    
    printf("Dati estratti:\n");
    // Successive chiamate: passa NULL
    while (token != NULL) {
        printf("- %s\n", token);
        token = strtok(NULL, delimitatori);
    }
    
    return 0;
}
⚠️ Attenzione con strtok()

strtok() modifica la stringa originale sostituendo i delimitatori con '\0'. Se devi preservare la stringa originale, fai una copia prima.

3.7 Tabella Riepilogativa delle Funzioni

Funzione Descrizione Utilizzo
strlen(str) Restituisce la lunghezza (esclude '\0') Calcolare dimensione stringa
strcpy(dest, src) Copia src in dest Duplicare stringhe (ATTENZIONE: buffer overflow)
strncpy(dest, src, n) Copia max n caratteri Copia sicura con limite
strcat(dest, src) Concatena src a dest Unire stringhe
strncat(dest, src, n) Concatena max n caratteri Concatenazione sicura
strcmp(s1, s2) Confronta due stringhe Verifica uguaglianza
strchr(str, ch) Trova prima occorrenza di ch Cercare un carattere
strstr(str, substr) Trova prima occorrenza di substr Cercare una sottostringa
strtok(str, delim) Divide stringa in token Parsing di testi

⌨️ Input e Output di Stringhe

4.1 Output con printf()

#include <stdio.h>

int main() {
    char nome[] = "Mario";
    char cognome[] = "Rossi";
    
    printf("Nome: %s\n", nome);
    printf("%s %s\n", nome, cognome);
    printf("Prime 3 lettere: %.3s\n", nome);
    
    return 0;
}

4.2 Input Sicuro con fgets()

Mostra esempio input sicuro
#include <stdio.h>
#include <string.h>

int main() {
    char buffer[100];
    
    printf("Inserisci una frase: ");
    fgets(buffer, sizeof(buffer), stdin);
    
    // Rimuovi il newline
    size_t len = strlen(buffer);
    if (len > 0 && buffer[len-1] == '\n') {
        buffer[len-1] = '\0';
    }
    
    printf("Hai scritto: %s\n", buffer);
    
    return 0;
}
✓ Vantaggi di fgets()
  • Legge l'intera linea, inclusi gli spazi
  • Specifica la dimensione massima del buffer
  • Non causa buffer overflow

🔧 Allocazione Dinamica di Stringhe

Mostra esempio allocazione dinamica
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    size_t lunghezza = 100;
    
    // Alloca memoria per lunghezza caratteri + 1 per '\0'
    char *stringa = (char *)malloc((lunghezza + 1) * sizeof(char));
    
    if (stringa == NULL) {
        fprintf(stderr, "Errore: memoria insufficiente!\n");
        return 1;
    }
    
    strcpy(stringa, "Testo dinamico");
    printf("%s\n", stringa);
    
    // IMPORTANTE: libera la memoria!
    free(stringa);
    
    return 0;
}

⚠️ Errori Comuni e Come Evitarli

❌ 1. Buffer Overflow
char buffer[10];
strcpy(buffer, "Stringa troppo lunga");  // DISASTRO!

Soluzione: Usa strncpy() o controlla le dimensioni.

❌ 2. Dimenticare il Terminatore Null
char str[4] = {'C', 'I', 'A', 'O'};  // Manca '\0'!
printf("%s\n", str);  // Comportamento indefinito!
❌ 3. Confrontare Stringhe con ==
char str1[] = "Hello";
char str2[] = "Hello";

if (str1 == str2) {  // SBAGLIATO: confronta indirizzi!
    // Usa strcmp(str1, str2) == 0
}

✅ Best Practices per Stringhe Sicure

✓ Regole d'Oro
  1. Inizializza sempre: char buffer[100] = {0};
  2. Usa funzioni "n": Preferisci strncpy(), strncat()
  3. Controlla i limiti: Verifica sempre lo spazio disponibile
  4. Usa sizeof(): strncpy(dest, src, sizeof(dest) - 1);
  5. Preferisci fgets() a scanf(): Per input utente
  6. Controlla malloc(): Verifica sempre != NULL
  7. Libera la memoria: Ogni malloc() ha il suo free()

💪 Esercizi Proposti

Esercizio 1: Inversione di Stringa

Scrivi una funzione che inverte una stringa sul posto.

void inverti_stringa(char *str);

// Input: "Ciao"
// Output: "oaiC"

Esercizio 2: Conta Vocali

Conta il numero di vocali e consonanti in una stringa.

Esercizio 3: Palindromo

Verifica se una stringa è un palindromo.

int e_palindromo(const char *str);

// "anna" → 1
// "radar" → 1
// "ciao" → 0

🎯 Quiz di Verifica

Domanda 1

Quale carattere termina una stringa in C?

Domanda 2

Quale funzione è più sicura per copiare stringhe?

Domanda 3

Come si confrontano correttamente due stringhe?

🎯 Conclusioni

Le stringhe in C, sebbene implementate in modo più "primitivo" rispetto ad altri linguaggi, offrono un controllo completo e prestazioni eccellenti. La chiave per lavorare efficacemente con le stringhe in C è comprendere:

✓ Cosa Abbiamo Imparato
  • Fondamenti: Le stringhe sono array di char terminati da '\0'
  • Funzioni standard: strlen(), strcpy(), strcmp() e varianti
  • Sicurezza: Evitare buffer overflow, usare funzioni "n"
  • Input/Output: Preferire fgets() a scanf()

📚 Riferimenti e Approfondimenti

Per approfondire ulteriormente gli argomenti trattati, consultare: